# -*- coding: UTF-8 -*-
# File date: Hi_2022/12/12 18:34
# File_name: Image_tool_GUI.py
import os
import time
from tkinter.filedialog import askdirectory, askopenfilenames
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.tooltip import ToolTip
from ttkbootstrap.dialogs.dialogs import Messagebox
from log_out import log_out
from threading import Thread
from pillow_method import pillow_tool, preview_gif
from PIL import UnidentifiedImageError
from PIL.Image import DecompressionBombWarning
from typing import Tuple
from windnd import hook_dropfiles

logger = log_out()
logger.success("日志初始化")

# 配置参数
config = {"size": dict(),  # size区域参数和组件
          "format": dict(),  # format区域参数和路径
          "compress": dict(),  # compress区参数和组件
          "save_conf": dict(),  # save区域参数和组件
          "file_tree": dict(),  # 表格区域参数和组件
          "image_preview": dict(),  # 预览区参数和组件
          "frame": dict(),  # 主要的几个frame
          "top": dict()  # 文件过滤窗口
          }


class ImageTools(pillow_tool):

    def __init__(self, win: ttk.Window):
        self.root = win

        self.entry_int_validate = self.root.register(self.check_int)
        self.entry_float_validate = self.root.register(self.check_float_int)

        # 过滤参数,防止关闭窗口丢失
        self.aspectRatio_w = ttk.StringVar(value="1")  # 宽过滤比例
        self.aspectRatio_h = ttk.StringVar(value="1")  # 高过滤比例
        self.check_w = ttk.StringVar()  # 过滤宽
        self.check_h = ttk.StringVar()  # 过滤高
        self.st_size_mb = ttk.StringVar()  # 过滤文件大小mb变量
        self.st_size_kb = ttk.StringVar()  # 过滤文件大小kb变量
        self.status_1 = ttk.BooleanVar()
        self.status_L = ttk.BooleanVar()
        self.status_P = ttk.BooleanVar()
        self.status_I = ttk.BooleanVar()
        self.status_F = ttk.BooleanVar()
        self.status_RGB = ttk.BooleanVar()
        self.status_RGBA = ttk.BooleanVar()
        self.status_YCbCr = ttk.BooleanVar()
        self.status_LAB = ttk.BooleanVar()
        self.status_HSV = ttk.BooleanVar()
        self.status_CMYK = ttk.BooleanVar()

        self.win_init()

    def win_init(self):
        """窗口参数初始化"""
        logger.success("主窗口初始化")

        # 窗口关闭回调
        self.root.protocol('WM_DELETE_WINDOW', self.close_root)

        # 窗口标题
        self.root.title("ImageTools")
        logger.success(f"设置主窗口标题：{self.root.title()}")

        # 设置主窗口宽高和位置
        width = 1400  # 窗口长宽
        height = 800
        win_w = self.root.winfo_screenwidth()  # 桌面长宽
        win_h = self.root.winfo_screenheight()
        self.root.geometry(f"{width}x{height}+{(win_w - width) // 2}+{(win_h - height) // 2}")  # 设置窗口位置和宽高
        self.root.update()
        logger.success(f"设置窗口宽{self.root.winfo_width()} 高{self.root.winfo_height()} 向右偏移{self.root.winfo_pointerx()} 向下偏移{self.root.winfo_pointery()}")

        # 不可调整大小
        # self.root.resizable(False, False)
        # logger.success(f"设置窗口不可调节大小")

        # 设置最小尺寸
        self.root.minsize(1400, 800)

        # 窗口置顶
        # self.root.wm_attributes('-topmost', 1)
        # logger.success("设置窗口置顶")

        # 主框架
        root_frame = ttk.Frame(self.root)
        root_frame.place(relx=0.002, rely=0.005, relwidth=0.996, relheight=0.995)

        # 参数修改区
        parameter_area = ttk.Frame(root_frame)
        parameter_area.place(relx=0, rely=0.71, relwidth=0.5, relheight=0.29)  # 参数区域

        # 添加到配置文件
        config["frame"]["root_frame"] = root_frame
        config["frame"]["parameter_area"] = parameter_area

        # 窗口图标
        self.root.iconphoto(True, self.get_tk_Image("./image/win_icon.png"))

        # 各个界面
        self.add_button_layout()  # 文件按钮
        self.file_tree_layout()  # 文件树
        self.image_size_layout()  # 尺寸修改区
        self.image_format_layout()  # 格式修改区
        self.image_commpress_layout()  # 压缩修改区
        self.image_out_layout()  # 保存参数区
        self.image_preview_layout()  # 预览区

    def add_button_layout(self):
        # 顶部添加文件菜单项展示区
        add_menu = ttk.Frame(config["frame"]["root_frame"])
        add_file = ttk.Button(add_menu, text="添加文件", bootstyle="outline", cursor="hand2")
        add_dir = ttk.Button(add_menu, text="添加目录", bootstyle="outline", cursor="hand2")
        remove_select = ttk.Button(add_menu, text="移除选择", bootstyle="outline", cursor="hand2")
        remove_all = ttk.Button(add_menu, text="全部移除", bootstyle="outline", cursor="hand2")
        filtration_but = ttk.Button(add_menu, text="文件过滤", bootstyle="outline")
        start_but = ttk.Button(add_menu, text="开始处理", bootstyle="success-outline", cursor="hand2")

        # 气泡提示
        ToolTip(add_file, text="点击这里添加文件,也可直接拖动文件到窗口添加文件,程序自动忽略非图片文件", topmost=True)
        ToolTip(widget=add_dir, text="点击这里添加目录,也可直接拖动目录到窗口添加文件,程序自动忽略非图片文件", topmost=True)
        ToolTip(widget=remove_select, text="移除选择项\n可按住Ctrl或Shift多选", topmost=True)
        ToolTip(widget=remove_all, text="点击这里清空列表", topmost=True)
        ToolTip(widget=filtration_but, text="点击这里筛选已添加文件", topmost=True)
        ToolTip(widget=start_but, text="点击这里开始处理图像", topmost=True)

        # 方法绑定
        add_file.config(command=self.add_file)
        add_dir.config(command=self.add_dir)
        remove_select.config(command=self.remove_select)
        remove_all.config(command=self.remove_all_row)
        filtration_but.config(command=self.top_layout)
        start_but.config(command=self.start_main)

        # 顶部添加文件菜单项展示区
        add_menu.place(relx=0, rely=0.65, relwidth=0.5, relheight=0.06)

        add_file.place(relx=0.01428, rely=0.15, relwidth=0.15, relheight=0.7)
        add_dir.place(relx=0.17856, rely=0.15, relwidth=0.15, relheight=0.7)
        remove_select.place(relx=0.34284, rely=0.15, relwidth=0.15, relheight=0.7)
        remove_all.place(relx=0.50712, rely=0.15, relwidth=0.15, relheight=0.7)
        filtration_but.place(relx=0.6714, rely=0.15, relwidth=0.15, relheight=0.7)
        start_but.place(relx=0.83568, rely=0.15, relwidth=0.15, relheight=0.7)

        # 添加到配置文件
        config["file_tree"]["add_menu"] = add_menu
        config["file_tree"]["add_file"] = add_file
        config["file_tree"]["add_dir"] = add_dir
        config["file_tree"]["remove_select"] = remove_select
        config["file_tree"]["remove_all"] = remove_all
        config["file_tree"]["start_but"] = start_but

        logger.success("添加/移除按钮区域初始化完成")

    def file_tree_layout(self):
        # 添加文件展示区
        table_area = ttk.LabelFrame(config["frame"]["root_frame"], text="文件列表")

        # 文件目录树
        columns = ["file_name", "size", "mode", "st_size", "path"]  # 列名
        image_tree = ttk.Treeview(table_area, show="headings", columns=columns, selectmode="extended")

        image_tree.column("file_name", anchor="center", width=200)  # 设置表格文字居中
        image_tree.column("size", anchor="center", width=100)
        image_tree.column("mode", anchor="center", width=70)
        image_tree.column("st_size", anchor="w", width=100)
        image_tree.column("path", anchor="w", width=600)
        image_tree.heading("file_name", text="文件名")  # 设置表格头部标题
        image_tree.heading("size", text="分辨率")
        image_tree.heading("mode", text="模式")
        image_tree.heading("st_size", text="大小")
        image_tree.heading("path", text="路径")

        # xy滚动条
        y_Scrollbar = ttk.Scrollbar(table_area, command=image_tree.yview, orient="vertical", bootstyle="light-round")
        x_Scrollbar = ttk.Scrollbar(table_area, command=image_tree.xview, orient="horizontal", bootstyle="light-round")
        image_tree.config(yscrollcommand=y_Scrollbar.set, xscrollcommand=x_Scrollbar.set)

        # 拖拽文件处理
        hook_dropfiles(self.root, func=self.dragged_files)

        # 选择文件绑定方法更新预览区
        image_tree.bind("<<TreeviewSelect>>", self.select_preview)

        # 布局
        table_area.place(relx=0, rely=0.0, relwidth=0.5, relheight=0.65)  # 添加文件展示区
        image_tree.place(relx=0, rely=0., relwidth=0.975, relheight=0.97)

        y_Scrollbar.place(relx=0.98, rely=0, relwidth=0.02, relheight=1)
        x_Scrollbar.place(relx=0, rely=0.975, relwidth=0.98, relheight=0.025)

        # 添加到配置文件
        config["file_tree"]["show_area"] = table_area
        config["file_tree"]["image_tree"] = image_tree
        config["file_tree"]["open_file_list"] = list()

        logger.success("文件表格区域初始化完成")

    def image_size_layout(self):
        conf = config["size"]
        # 参数
        conf["state"] = ttk.BooleanVar(value=True)  # 尺寸参数显示
        conf["option"] = ttk.StringVar(value="cut")  # 尺寸缩放类型开关
        conf["width"] = ttk.StringVar(value="1920")  # 修改尺寸后图片宽
        conf["height"] = ttk.StringVar(value="1080")  # 修改尺寸后图片高

        # 修改尺寸区
        size_show = ttk.Checkbutton(self.root, text="尺寸", variable=conf["state"], onvalue=True, offvalue=False, cursor="hand2")
        size_area = ttk.LabelFrame(config["frame"]["parameter_area"], text="尺寸", labelwidget=size_show, labelanchor="n")

        equal = ttk.Radiobutton(size_area, text="等比", variable=conf["option"], value="equal_ratio", cursor="hand2")  # 等比
        cut = ttk.Radiobutton(size_area, text="剪裁", variable=conf["option"], value="cut", cursor="hand2")  # 剪裁
        stretch = ttk.Radiobutton(size_area, text="拉伸", variable=conf["option"], value="stretch", cursor="hand2")  # 拉伸
        size_label = ttk.Label(size_area, text="尺寸:")
        width_entry = ttk.Entry(size_area, textvariable=conf["width"], justify="left")
        x_label = ttk.Label(size_area, text="x", anchor="center")
        height_entry = ttk.Entry(size_area, textvariable=conf["height"], justify="left")

        # 尺寸开关提示气泡
        ToolTip(size_show, text="点击这里开启/关闭修改尺寸,关闭后仅更新图像格式图像尺寸不会改变", topmost=True)
        ToolTip(equal, text="等比缩放图像,缩放到长或宽达到要求尺寸不变形也不损失图像,但是只有长或宽一个达到要求尺寸", topmost=True)
        ToolTip(cut, text="剪裁缩放图像,缩放到长或宽达到要求尺寸后居中裁剪到要求尺寸,不变形但当图像宽高比例和要求尺寸对应比例不一致时会损失损失部门图像,长宽均会达到要求尺寸", topmost=True)
        ToolTip(stretch, text="强制拉伸图像达到要求尺寸,不损失图像但大概率图像会扭曲变形,不建议使用", topmost=True)
        ToolTip(width_entry, text="缩放后要求的图像宽,仅支持输入大于零的整数", topmost=True)
        ToolTip(height_entry, text="缩放后要求的图像高,仅支持输入大于零的整数", topmost=True)

        # 宽高输入值校验
        width_entry.config(validate='key', validatecommand=(self.entry_int_validate, '%P'))
        height_entry.config(validate='key', validatecommand=(self.entry_int_validate, '%P'))

        # 方法绑定
        size_show.config(command=self.updata_size_show)

        # 布局
        size_area.place(relx=0, rely=0.0, relwidth=0.25, relheight=1)  # 修改尺寸区

        equal.place(relx=0.03, rely=0.2, relwidth=0.3, relheight=0.15)  # 等比
        cut.place(relx=0.33, rely=0.2, relwidth=0.3, relheight=0.15)  # 剪裁
        stretch.place(relx=0.63, rely=0.2, relwidth=0.3, relheight=0.15)  # 拉伸

        size_label.place(relx=0.03, rely=0.6, relwidth=0.22, relheight=0.15)  # 长宽提示label
        width_entry.place(relx=0.22, rely=0.6, relwidth=0.29, relheight=0.15)  # 宽
        x_label.place(relx=0.52, rely=0.6, relwidth=0.07, relheight=0.15)  # 乘号
        height_entry.place(relx=0.59, rely=0.6, relwidth=0.29, relheight=0.15)  # 高

        # 添加到配置
        config["size"]["size_area"] = size_area
        config["size"]["size_show"] = size_show

        config["size"]["equal_ratio"] = equal
        config["size"]["cut"] = cut
        config["size"]["stretch"] = stretch

        config["size"]["width_entry"] = width_entry
        config["size"]["height_entry"] = height_entry

        logger.success("尺寸区域初始化完成")

    def image_format_layout(self):
        conf = config["format"]
        conf["state"] = ttk.BooleanVar(value=True)  # 尺寸区显示开关
        conf["framerate"] = ttk.StringVar(value="40")  # 尺寸区显示开关

        # 修改格式区
        format_show = ttk.Checkbutton(self.root, text="格式", cursor="hand2", variable=conf["state"], onvalue=True, offvalue=False)
        format_area = ttk.LabelFrame(config["frame"]["parameter_area"], text="格式", labelwidget=format_show, labelanchor="n")

        format_list = ["png", "jpg", "webp", "gif", "ico", "bmp"]  # 暂时支持的格式
        # 格式选择下下拉框
        format_label = ttk.Label(format_area, text="格式:")
        format_combobox = ttk.Combobox(format_area, values=format_list, state="readonly", font=("黑体", 11))

        # ico多尺寸输入框
        ico_size_text = ttk.ScrolledText(format_area, font=("黑体", 11))
        size_str = "16*16\n24*24\n32*32\n48*48\n64*64\n96*96\n128*128\n192*192\n256*256"
        ico_size_text.delete(0.0, ttk.END)
        ico_size_text.insert(0.0, size_str)  # 插入默认值

        # gif帧间隔输入框
        gif_area = ttk.Frame(format_area)
        gif_framerate_label = ttk.Label(gif_area, text="帧间隔(ms):")
        gif_framerate_Entry = ttk.Entry(gif_area, textvariable=conf["framerate"], justify="left")

        # gif帧间隔输入值校验
        gif_framerate_Entry.config(validate="key", validatecommand=(self.entry_int_validate, "%P"))

        # 格式开关气泡提示
        ToolTip(format_show, text="点击这里开启/关闭修改图片格式,只设置尺寸参数关闭格式开关图像将会以原来的格式保存", topmost=True)
        format_combobox_tips = ToolTip(format_combobox, text="点击选择转换后图片格式", topmost=True)
        ToolTip(ico_size_text, text="ico可以设置多组尺寸,最大支持设置256*256,多组尺寸换行填写\n长宽不相等时等比缩放,以先达到要求尺寸的宽或高作为缩放系数", topmost=True)
        ToolTip(gif_framerate_Entry, text="转换为gif每一帧的间隔时间,以毫秒为单位,仅支持输入大于零的整数", topmost=True)

        # 方法绑定
        format_show.config(command=self.updata_format_show)
        format_combobox.bind("<<ComboboxSelected>>", self.updata_Combobox_format)

        # 把对象加入config
        config["format"]["format_area"] = format_area
        config["format"]["format_show"] = format_show
        config["format"]["format_combobox"] = format_combobox
        config["format"]["ico_size_text"] = ico_size_text
        config["format"]["gif_area"] = gif_area
        config["format"]["gif_framerate_label"] = gif_framerate_label
        config["format"]["gif_framerate_Entry"] = gif_framerate_Entry
        config["format"]["format_combobox_tips"] = format_combobox_tips

        # 布局
        format_area.place(relx=0.25, rely=0, relwidth=0.25, relheight=1)  # 修改格式区
        format_label.place(relx=0.03, rely=0.2, relwidth=0.17, relheight=0.16)  # 格式提示label
        format_combobox.place(relx=0.2, rely=0.2, relwidth=0.7, relheight=0.16)  # 格式选择下拉框

        gif_framerate_label.place(relx=0, rely=0.03, relwidth=0.39, relheight=1)
        gif_framerate_Entry.place(relx=0.39, rely=0.23, relwidth=0.39, relheight=0.7)

        logger.success("格式区域初始化完成")

    def image_commpress_layout(self):
        conf = config["compress"]
        # 参数
        conf["state"] = ttk.BooleanVar(value=False)  # 压缩质量参数显示
        conf["quality"] = ttk.IntVar()  # 0-95,默认75

        # 压缩参数区
        compress_show = ttk.Checkbutton(self.root, text="质量", variable=conf["state"], onvalue=True, offvalue=False, state="disable", cursor="hand2")
        compress_area = ttk.LabelFrame(config["frame"]["parameter_area"], text="质量", labelwidget=compress_show, labelanchor="n")

        quality_value_label = ttk.Label(compress_area, textvariable=conf["quality"])
        quality_scale = ttk.Scale(compress_area, variable=conf["quality"], orient="horizontal", state="disable", cursor="hand2")

        # 压缩开关气泡提示
        ToolTip(compress_show, text="当选择格式为png/jpg/webp时,点击这里开启/关闭自定义图片质量\n不选择压缩级别会自动设置默认压缩级别", topmost=True)
        ToolTip(quality_scale, text="滑块位置越靠右侧转换图片质量越好", topmost=True)

        # 方法绑定
        quality_scale.config(command=self.updata_quality_scale)
        compress_show.config(command=self.updata_compress_show)

        # 布局
        compress_area.place(relx=0.5, rely=0, relwidth=0.25, relheight=1)  # 压缩质量参数区
        quality_value_label.place(relx=0.45, rely=0.4, relwidth=0.1, relheight=0.15)
        quality_scale.place(relx=0.03, rely=0.5, relwidth=0.94, relheight=0.17)  # 质量滑块

        # 添加到配置
        conf["compress_area"] = compress_area
        conf["compress_show"] = compress_show
        conf["quality_value_label"] = quality_value_label
        conf["quality_scale"] = quality_scale

        logger.success("压缩区域初始化完成")

    def image_out_layout(self):
        conf = config["save_conf"]
        conf["output_select"] = ttk.StringVar(value="diy_dir_check")  # 原目录/自定义选项
        conf["d_structure"] = ttk.BooleanVar(value=False)  # 保持目录结构开关
        conf["save_path"] = ttk.StringVar()  # 自定义保存路径

        # 保存选项区
        save_option_area = ttk.LabelFrame(config["frame"]["parameter_area"], text="保存选项", labelanchor="n")
        # 原目录选项
        same_dir_check = ttk.Radiobutton(save_option_area, text="原目录", variable=conf["output_select"], value="same_dir_check", cursor="hand2")
        # 自定义目录选项
        diy_dir_check = ttk.Radiobutton(save_option_area, text="自定义", variable=conf["output_select"], value="diy_dir_check", cursor="hand2")
        # 保持目录结构选项
        dir_structure = ttk.Checkbutton(save_option_area, text="保持目录结构", variable=conf["d_structure"], offvalue=False, onvalue=True, cursor="hand2")
        # 路径entry
        check_save_path = ttk.Button(save_option_area, text="自定义保存目录", bootstyle="outline", cursor="hand2")

        # 压缩开关气泡提示
        ToolTip(same_dir_check, text="将文件列表所有图像处理后在原本的目录保存,新文件文件名会加时间戳防重名", topmost=True)
        ToolTip(diy_dir_check, text="设置自定义输出目录,将文件列表所有图像处理后统一保存在一个目录下", topmost=True)
        ToolTip(dir_structure, text="设置自定义保存目录的前提下,检查每个文件所在目录,并在保存目录下新建文件夹使目录结构保持和原目录一致", topmost=True)
        diy_label_tooltip = ToolTip(check_save_path, text="点击此处设置自定义保存目录,点击后进入选择保存目录", topmost=True)

        # 方法绑定
        check_save_path.config(command=self.select_save_path)
        same_dir_check.config(command=self.updata_diy_dir)
        diy_dir_check.config(command=self.updata_diy_dir)

        # 布局
        save_option_area.place(relx=0.75, rely=0, relwidth=0.25, relheight=1)  # 保存参数选项区

        same_dir_check.place(relx=0.03, rely=0.2, relwidth=0.4, relheight=0.15)  # 原目录
        diy_dir_check.place(relx=0.42, rely=0.2, relwidth=0.4, relheight=0.15)  # 自定义

        dir_structure.place(relx=0.03, rely=0.5, relwidth=0.6, relheight=0.15)  # 保持目录结构选项
        check_save_path.place(relx=0.03, rely=0.65, relwidth=0.94, relheight=0.15)  # 目录提示和选择label

        # 保存到config
        conf["save_option_area"] = save_option_area
        conf["same_dir_check"] = same_dir_check
        conf["diy_dir_check"] = diy_dir_check
        conf["dir_structure"] = dir_structure
        conf["check_save_path"] = check_save_path
        conf["diy_label_tooltip"] = diy_label_tooltip

        logger.success("输出参数区域初始化完成")

    def image_preview_layout(self):
        # 预览区
        preview_area = ttk.LabelFrame(config["frame"]["root_frame"], text="预览区")
        preview_label = ttk.Label(preview_area, anchor="center")
        ttk.Sizegrip(self.root, bootstyle="light").pack(side="bottom", anchor="se")  # 尺寸手柄

        # 布局
        preview_area.place(relx=0.5, rely=0, relwidth=0.5, relheight=1)  # 预览区
        preview_label.place(relx=0, rely=0, relwidth=1, relheight=1)

        # 添加到配置
        config["image_preview"]["preview_area"] = preview_area
        config["image_preview"]["preview_label"] = preview_label

        # 预览初始化
        sizes = self.get_preview_size()
        show_gif_image = preview_gif("image/win_icon.png", preview_label, sizes)
        preview_label.after_idle(self.preview_image, "image/win_icon.png", preview_label, sizes, show_gif_image)
        config["image_preview"]["previewing"] = self.get_resource_path("image/win_icon.png")
        logger.success(f"加载默认预览图:{self.get_resource_path('./image/win_icon.png')}")

        # 添加到配置
        config["image_preview"]["preview_area"] = preview_area
        config["image_preview"]["preview_label"] = preview_label
        config["image_preview"]["show_gif_image"] = show_gif_image

        logger.success("图片预览区域初始化完成")

    def top_layout(self):
        logger.success("打开文件过滤窗口")

        top = ttk.Toplevel(title="文件过滤", minsize=(400, 500), resizable=(True, True))

        # 窗口居中
        screen_width = self.root.winfo_screenwidth()
        screen_height = self.root.winfo_screenheight()

        top.geometry(f"+{(screen_width - 500) // 2}+{(screen_height - 300) // 2}")
        top.attributes("-topmost", True)  # 置顶

        # 关闭窗口回调
        top.protocol('WM_DELETE_WINDOW', self.close_top)

        # 保存参数
        config["top"]["top"] = top

        # 宽高比例过滤
        def ft_size_proportion_layout():
            size_proportion_filtration = ttk.LabelFrame(top, text="宽高比例过滤")
            aspectRatio_tips = ttk.Label(size_proportion_filtration, text="宽高比例:")
            width_proportion_entry = ttk.Entry(size_proportion_filtration, textvariable=self.aspectRatio_w, justify="center")
            colon_label = ttk.Label(size_proportion_filtration, text=":", anchor="center")
            height_proportion_entry = ttk.Entry(size_proportion_filtration, textvariable=self.aspectRatio_h, justify="center")

            size_proportion_less = ttk.Button(size_proportion_filtration, text="移除小于设置比例", bootstyle="outline")
            size_proportion_more = ttk.Button(size_proportion_filtration, text="移除大于设置比例", bootstyle="outline")

            # 宽高比例输入值校验
            width_proportion_entry.config(validate='key', validatecommand=(self.entry_float_validate, '%P'))
            height_proportion_entry.config(validate='key', validatecommand=(self.entry_float_validate, '%P'))

            # 气泡提示
            ToolTip(aspectRatio_tips, text="宽/高得到宽高比例,判断所有图像自身的宽高比和过滤界限宽高比大小进行过滤\ntips:设置过滤1:1,大于按钮即移除列表所有宽大于高的图如电脑壁纸,小于按钮即移除列表所有高大于宽的图如手机壁纸", topmost=True)
            ToolTip(width_proportion_entry, text="宽/高得到宽高比例,这里输入宽比例值,仅支持输入大于零的小数或整数", topmost=True)
            ToolTip(height_proportion_entry, text="宽/高得到宽高比例,这里输入高比例值,仅支持输入大于零的小数或整数", topmost=True)
            ToolTip(size_proportion_less, text="移除列表中所有图像宽高比小于过滤比例的图像", topmost=True)
            ToolTip(size_proportion_more, text="移除列表中所有图像宽高比大于过滤比例的图像", topmost=True)

            # 方法绑定
            size_proportion_less.config(command=lambda: self.ft_aspectRatio(-1))
            size_proportion_more.config(command=lambda: self.ft_aspectRatio(1))

            # 布局
            size_proportion_filtration.place(relx=0, rely=0, relwidth=1, relheight=0.25)

            aspectRatio_tips.place(relx=0.291, rely=0.2, relwidth=0.15, relheight=0.24)
            width_proportion_entry.place(relx=0.441, rely=0.2, relwidth=0.12, relheight=0.24)
            colon_label.place(relx=0.561, rely=0.2, relwidth=0.028, relheight=0.24)
            height_proportion_entry.place(relx=0.589, rely=0.2, relwidth=0.12, relheight=0.24)

            size_proportion_less.place(relx=0.09333, rely=0.64, relwidth=0.36, relheight=0.29)
            size_proportion_more.place(relx=0.54666, rely=0.64, relwidth=0.36, relheight=0.29)

        # 宽高过滤
        def ft_size_layout():
            size_filtration = ttk.LabelFrame(top, text="宽高尺寸过滤")

            width_tips = ttk.Label(size_filtration, text="宽:", anchor="center")
            width_entry = ttk.Entry(size_filtration, textvariable=self.check_w, justify="center")
            height_tips = ttk.Label(size_filtration, text="高:", anchor="center")
            height_entry = ttk.Entry(size_filtration, textvariable=self.check_h, justify="center")
            size_less = ttk.Button(size_filtration, text="移除宽高小于设置值", bootstyle="outline")
            size_more = ttk.Button(size_filtration, text="移除宽高大于设置值", bootstyle="outline")

            # 宽高比例输入值校验
            width_entry.config(validate='key', validatecommand=(self.entry_int_validate, '%P'))
            height_entry.config(validate='key', validatecommand=(self.entry_int_validate, '%P'))

            # 气泡提示
            ToolTip(width_tips, text="根据图像的实际宽高来过滤图像\n仅输入宽或高,代表只要列表文件的宽或高大于或小于(对应下方两个按钮)设置阈值就会移除\n同时设置宽高界限需要宽和高同时都小于或大于设置界限才会过滤\n可分成两次过滤宽或高任意不达标的条件\n宽或高需要最少填写一个不能都填写0或者不填写", topmost=True)
            ToolTip(height_tips, text="根据图像的实际宽高来过滤图像\n仅输入宽或高,代表只要列表文件的宽或高大于或小于(对应下方两个按钮)设置阈值就会移除\n同时设置宽高界限需要宽和高同时都小于或大于设置界限才会过滤\n可分成两次过滤宽或高任意不达标的条件\n宽或高需要最少填写一个不能都填写0或者不填写", topmost=True)
            ToolTip(width_entry, text="过滤列表中图像宽的界限值,可不输入或输入大于等于0的整数,输入0或不输入代表不把该值作为过滤条件\n如输入宽0高100 点下移除宽高小于设置值按钮则只会移除所有高度小于100的文件\n同时输入宽高需要宽和高都不满足过滤宽高条件时才会移除对应列表文件", topmost=True)
            ToolTip(height_entry, text="过滤列表中图像高的界限值,可不输入或输入大于等于0的整数,输入0或不输入代表不把该值作为过滤条件\n如输入宽100高0 点下移除宽高小于设置值按钮则只会移除所有宽度小于100的文件\n同时输入宽高需要宽和高都不满足过滤宽高条件时才会移除对应列表文件", topmost=True)
            ToolTip(size_less, text="根据输入值判断移除宽/高/宽高小于设置宽/高/宽高的图像", topmost=True)
            ToolTip(size_more, text="根据输入值判断移除宽/高/宽高大于设置宽/高/宽高的图像", topmost=True)

            # 方法绑定
            size_less.config(command=lambda: self.ft_size(-1))
            size_more.config(command=lambda: self.ft_size(1))

            # 布局
            size_filtration.place(relx=0, rely=0.25, relwidth=1, relheight=0.25)

            width_tips.place(relx=0.2, rely=0.2, relwidth=0.08, relheight=0.24)
            width_entry.place(relx=0.28, rely=0.2, relwidth=0.12, relheight=0.24)
            height_tips.place(relx=0.5, rely=0.2, relwidth=0.08, relheight=0.24)
            height_entry.place(relx=0.58, rely=0.2, relwidth=0.12, relheight=0.24)

            size_less.place(relx=0.09333, rely=0.64, relwidth=0.36, relheight=0.29)
            size_more.place(relx=0.54666, rely=0.64, relwidth=0.36, relheight=0.29)

        # 文件大小过滤
        def ft_st_size_layout():
            st_size = ttk.LabelFrame(top, text="文件大小过滤")
            st_size_tips = ttk.Label(st_size, text="文件大小:", anchor="center")
            st_size_mb_entry = ttk.Entry(st_size, textvariable=self.st_size_mb, justify="center")
            st_size_mb_tips = ttk.Label(st_size, text="mb", anchor="center")
            st_size_kb_entry = ttk.Entry(st_size, textvariable=self.st_size_kb, justify="center")
            st_size_kb_tips = ttk.Label(st_size, text="kb", anchor="center")
            st_size_less = ttk.Button(st_size, text="移除文件小于设置值", bootstyle="outline")
            st_size_more = ttk.Button(st_size, text="移除文件大于设置值", bootstyle="outline")

            # 文件大小输入值校验
            st_size_mb_entry.config(validate='key', validatecommand=(self.entry_float_validate, '%P'))
            st_size_kb_entry.config(validate='key', validatecommand=(self.entry_float_validate, '%P'))

            # 气泡提示
            ToolTip(st_size_tips, text="根据列表图像文件大小判断是否大于或小于设置阈值,移除满足条件的文件,mb和kb输入框需要最少输入一个才可正常过滤,最终会将mb和kb对应大小加在一起作为判断值", topmost=True)
            ToolTip(st_size_mb_entry, text="过滤mb单位,可不输入或输入大于等于0的整数/小数,不输入视作输入0\nmb和kb输入框最少需要填写一个", topmost=True)
            ToolTip(st_size_kb_entry, text="过滤kb单位,可不输入或输入大于等于0的整数/小数,不输入视作输入0\nmb和kb输入框最少需要填写一个", topmost=True)
            ToolTip(st_size_less, text="移除列表中文件大小小于kb加mb之和值得图像", topmost=True)
            ToolTip(st_size_more, text="移除列表中文件大小大于kb加mb之和值得图像", topmost=True)

            # 方法绑定
            st_size_less.config(command=lambda: self.ft_st_size(-1))
            st_size_more.config(command=lambda: self.ft_st_size(1))

            # 布局
            st_size.place(relx=0, rely=0.5, relwidth=1, relheight=0.25)

            st_size_tips.place(relx=0.205, rely=0.2, relwidth=0.14, relheight=0.24)
            st_size_mb_entry.place(relx=0.347, rely=0.2, relwidth=0.12, relheight=0.24)
            st_size_mb_tips.place(relx=0.467, rely=0.2, relwidth=0.08, relheight=0.24)
            st_size_kb_entry.place(relx=0.595, rely=0.2, relwidth=0.12, relheight=0.24)
            st_size_kb_tips.place(relx=0.715, rely=0.2, relwidth=0.08, relheight=0.24)

            st_size_less.place(relx=0.09333, rely=0.64, relwidth=0.36, relheight=0.29)
            st_size_more.place(relx=0.54666, rely=0.64, relwidth=0.36, relheight=0.29)

        # 文件显示模式过滤
        def ft_mode_layout():
            file_mode = ttk.LabelFrame(top, text="显示模式过滤")

            check_1 = ttk.Checkbutton(file_mode, text="1", onvalue=True, offvalue=False, variable=self.status_1)
            check_L = ttk.Checkbutton(file_mode, text="L", onvalue=True, offvalue=False, variable=self.status_L)
            check_P = ttk.Checkbutton(file_mode, text="P", onvalue=True, offvalue=False, variable=self.status_P)
            check_I = ttk.Checkbutton(file_mode, text="I", onvalue=True, offvalue=False, variable=self.status_I)
            check_F = ttk.Checkbutton(file_mode, text="F", onvalue=True, offvalue=False, variable=self.status_F)
            check_RGB = ttk.Checkbutton(file_mode, text="RGB", onvalue=True, offvalue=False, variable=self.status_RGB)
            check_RGBA = ttk.Checkbutton(file_mode, text="RGBA", onvalue=True, offvalue=False, variable=self.status_RGBA)
            check_YCbCr = ttk.Checkbutton(file_mode, text="YCbCr", onvalue=True, offvalue=False, variable=self.status_YCbCr)
            check_LAB = ttk.Checkbutton(file_mode, text="LAB", onvalue=True, offvalue=False, variable=self.status_LAB)
            check_HSV = ttk.Checkbutton(file_mode, text="HSV", onvalue=True, offvalue=False, variable=self.status_HSV)
            check_CMYK = ttk.Checkbutton(file_mode, text="CMYK", onvalue=True, offvalue=False, variable=self.status_CMYK)

            mode_but = ttk.Button(file_mode, text="移除选中模式", bootstyle="outline")

            # 气泡提示
            ToolTip(mode_but, text="根据勾选的显示模式作为判断,从列表移除所有图像显示模式为勾选模式的文件", topmost=True)

            # 方法绑定
            mode_but.config(command=self.ft_file_mode)

            # 布局
            file_mode.place(relx=0, rely=0.75, relwidth=1, relheight=0.25)

            check_1.place(relx=0.05, rely=0.1, relwidth=0.12, relheight=0.24)
            check_L.place(relx=0.17, rely=0.1, relwidth=0.12, relheight=0.24)
            check_P.place(relx=0.29, rely=0.1, relwidth=0.12, relheight=0.24)
            check_I.place(relx=0.41, rely=0.1, relwidth=0.12, relheight=0.24)
            check_F.place(relx=0.53, rely=0.1, relwidth=0.12, relheight=0.24)
            check_RGB.place(relx=0.65, rely=0.1, relwidth=0.15, relheight=0.24)
            check_RGBA.place(relx=0.8, rely=0.1, relwidth=0.15, relheight=0.24)

            check_YCbCr.place(relx=0.13, rely=0.34, relwidth=0.19, relheight=0.24)
            check_LAB.place(relx=0.32, rely=0.34, relwidth=0.18, relheight=0.24)
            check_HSV.place(relx=0.5, rely=0.34, relwidth=0.18, relheight=0.24)
            check_CMYK.place(relx=0.68, rely=0.34, relwidth=0.19, relheight=0.24)

            mode_but.place(relx=0.25, rely=0.6, relwidth=0.5, relheight=0.29)

        ft_size_proportion_layout()
        ft_size_layout()
        ft_st_size_layout()
        ft_mode_layout()

    def select_preview(self, event):
        """预览选中的图片"""
        widget: ttk.Treeview = event.widget
        select_item = widget.selection()[-1]  # 当前焦点item

        image_path = widget.item(select_item, 'values')[4]  # 当前焦点item的文件路径
        show_label = config["image_preview"]["preview_label"]  # 预览区label
        sizes = self.get_preview_size()  # 获取label区域宽高
        show_gif_image = config["image_preview"]["show_gif_image"]  # 显示动图类实例

        logger.success(f"选中回调,准备预览图片:{image_path}")
        # 更新正在预览图片
        config["image_preview"]["previewing"] = image_path

        try:
            widget.after_idle(self.preview_image, image_path, show_label, sizes, show_gif_image)
        except UnidentifiedImageError:
            logger.error(f"预览文件非图像文件:{image_path}")
            return

        except FileNotFoundError:
            logger.error(f"找不到预览文件:{image_path}")
            return

        except DecompressionBombWarning:
            logger.error(f"预览文件太大超过10^9个像素点,怀疑为图像炸弹:{image_path}")
            return

        except (ValueError, BaseException):
            logger.error(f"预览文件遇到未知异常")
            return

    def close_root(self):
        but_str = Messagebox.yesno(title="确认关闭窗口？", message="确认关闭窗口？", alert=True)  # , position=(835, 475)
        if but_str == "确认" or but_str == "Yes":
            logger.critical("销毁所有组件,退出程序")
            self.root.destroy()

    @staticmethod
    def close_top():
        logger.success("关闭文件过滤窗口")
        config["top"]["top"].destroy()

    def get_preview_size(self) -> Tuple[int, int]:
        """获取预览区域高宽"""
        self.root.update()
        # 预览label宽高变量# 更新预览区域高宽
        width = config["image_preview"]["preview_label"].winfo_width()
        height = config["image_preview"]["preview_label"].winfo_height()
        return width, height

    @staticmethod
    def select_save_path():
        """选择保存的目录,并更新选择按钮的提示信息"""
        ask_save_path = askdirectory()
        if ask_save_path == "":
            logger.success(f"未选择保存的目录,不执行")
            return
        logger.success(f"选择保存的目录：{ask_save_path}")
        config["save_conf"]["save_path"].set(ask_save_path)  # 自定义保存路径
        config["save_conf"]["check_save_path"].config(text=ask_save_path)
        config["save_conf"]["diy_label_tooltip"].text = f"{ask_save_path}\n可再次点击此处修改自定义保存目录"  # 修改提示

    def insert_value(self, file_path):
        """把数据插入并做校验"""
        try:
            line_value = self.get_image_info(file_path)
            config["file_tree"]["image_tree"].insert('', index="end", values=line_value)
            config["file_tree"]["open_file_list"].append(file_path)
            logger.info(f"添加文件:{file_path}")

        except UnidentifiedImageError:
            logger.info(f"文件非图像文件,插入跳过:{file_path}")

        except FileNotFoundError:
            logger.info(f"找不到文件,插入跳过:{file_path}")

        except DecompressionBombWarning:
            logger.error(f"文件太大超过10^9个像素点,怀疑为图像炸弹,插入跳过:{file_path}")

        except (ValueError, BaseException):
            logger.error(f"插入图像遇到位置异常,插入跳过:{file_path}")

    def inster_dir(self, all_file_path):
        """把一个目录中的所有图片插入"""
        logger.info("递归判断目录中所有文件夹和子文件夹中的文件插入")

        def _inter(all_path):
            logger.info(f"开始读取文件夹:{all_path}")
            for file_path in os.listdir(all_path):
                file_path = os.path.join(all_path, file_path)
                if os.path.isfile(file_path):  # 如果是文件则尝试添加
                    if file_path in config["file_tree"]["open_file_list"]:  # 已添加文件取消加入
                        logger.info(f"文件已添加,跳过:{file_path}")
                        continue
                    self.insert_value(file_path)

                elif os.path.isdir(file_path):  # 如果是目录则递归
                    logger.info(f"递归目录:{file_path}")
                    self.inster_dir(file_path)

                else:  # 如果都不是那可能目录已经被删除
                    logger.error(f"传入参数错误,跳过：{file_path}")
                    continue
            logger.success(f"所有文件读取插入完成：{all_file_path}")

        thread = Thread(target=_inter, args=(all_file_path,))
        thread.setDaemon(True)
        thread.start()

    def dragged_files(self, files):
        logger.info("拖拽插入文件")
        for file_path in files:
            file_path = file_path.decode('gbk')
            if os.path.isfile(file_path):  # 如果是文件
                if file_path in config["file_tree"]["open_file_list"]:  # 已添加文件取消加入
                    logger.info(f"文件已添加,跳过:{file_path}")
                    continue
                self.insert_value(file_path)

            elif os.path.isdir(file_path):  # 如果拖拽的是目录
                self.inster_dir(file_path)

    def add_file(self):
        """添加多个并将其中的图片加入列表"""
        logger.info("选择多个文件插入")
        files_path = askopenfilenames()
        if files_path == "":
            logger.success(f"未选择文件")
            return
        for file_path in files_path:
            if file_path in config["file_tree"]["open_file_list"]:  # 已添加文件取消加入
                logger.info(f"文件已添加,跳过:{file_path}")
                continue
            self.insert_value(file_path)

    def add_dir(self):
        """把添加的目录所有图片文件拿出来"""
        logger.info("选择目录插入")
        dir_path = askdirectory()
        if dir_path == "":
            logger.success(f"未选择打开的目录")
            return
        logger.success(f"打开目录,并在独立进程读取文件：{dir_path}")
        self.inster_dir(dir_path)

    def remove_select(self):
        """移除表格选择的行"""
        logger.info("从列表移除所选行")
        table: ttk.Treeview = config["file_tree"]["image_tree"]
        select_items = table.selection()  # 所有选中行
        logger.info(f"准备从文件树删除选择项：{', '.join([table.item(itemid, 'values')[4] for itemid in select_items])}")

        if not select_items:
            logger.info("文件列表为空")
            return

        for itemid in select_items:
            self.remove_item(itemid)

    def remove_all_row(self):
        """移除表格所有"""
        logger.info("清空列表所有已添加文件")
        table = config["file_tree"]["image_tree"]
        all_item = table.get_children()  # 根目录的所有item
        if not all_item:
            logger.info("文件列表为空")
            return

        # 停止循环换成默认图
        image_path = self.get_resource_path("./image/win_icon.png")
        show_label = config["image_preview"]["preview_label"]  # 预览区label
        sizes = self.get_preview_size()  # 获取label区域宽高
        show_gif_image = config["image_preview"]["show_gif_image"]  # 显示动图类实例
        self.root.after_idle(self.preview_image, image_path, show_label, sizes, show_gif_image)
        logger.success(f"移除全部,恢复默认预览图片:{image_path}")

        logger.info(f"删除所有数据：{','.join(all_item)}")

        for itemid in all_item:
            logger.info(f"从文件树删除：{table.item(itemid, 'values')[4]}")
            table.delete(itemid)

        logger.info("清空已添加文件列表")
        config["file_tree"]["open_file_list"].clear()

    def remove_item(self, itemid: str):
        """删除指定item数据"""
        logger.info("从列表移除指定item")
        previewing = config["image_preview"]["previewing"]
        table: ttk.Treeview = config["file_tree"]["image_tree"]
        item_path = table.item(itemid, 'values')[4]

        logger.info(f"准备删除图像：{item_path}")

        # 正在浏览文件被删除恢复默认预览
        if item_path == previewing:
            logger.info(f"正在预览图像：{previewing}")
            image_path = self.get_resource_path("./image/win_icon.png")
            logger.success(f"正在预览图像被移除,恢复默认预览图片:{image_path}")
            show_label = config["image_preview"]["preview_label"]  # 预览区label
            sizes = self.get_preview_size()  # 获取label区域宽高
            show_gif_image = config["image_preview"]["show_gif_image"]  # 显示动图类实例
            self.root.after_idle(self.preview_image, image_path, show_label, sizes, show_gif_image)

        logger.info(f"从已添加文件列表和表格删除:{item_path}")
        config["file_tree"]["open_file_list"].remove(item_path)
        table.delete(itemid)

    def ft_aspectRatio(self, compare_values):
        """
        过滤文件树中宽高比例小于设置比例的图片
        :param compare_values: 过滤参数,需要过滤大于设置比例的传入正数,小于传入负数
        """
        logger.info("按照宽高比过滤文件列表")
        aspectRatio_w = self.aspectRatio_w.get()
        aspectRatio_h = self.aspectRatio_h.get()
        if aspectRatio_w in ["", "0"] or aspectRatio_h in ["", "0"]:
            self.show_toast_error(title="宽高比填写错误", message="请正确填写宽高比,仅支持填入大于零的数字")
            logger.warning(f"宽高比({aspectRatio_w}:{aspectRatio_h})填写错误")
            return
        ft_aspectRatio = float(aspectRatio_w) / float(aspectRatio_h)
        logger.info(f"移除宽高比{'大于' if compare_values > 0 else '小于'}{ft_aspectRatio}")

        image_tree: ttk.Treeview = config["file_tree"]["image_tree"]  # 文件树
        all_item = list(image_tree.get_children())  # 所有item
        if not all_item:
            logger.info("已添加文件列表为空,不执行过滤")
            return

        for item in all_item:
            item_image_size = [float(w_h) for w_h in image_tree.item(item, "values")[1].split("x")]
            item_image_aspectRatio = item_image_size[0] / item_image_size[1]
            item_compare = item_image_aspectRatio - ft_aspectRatio
            if (compare_values > 0 and item_compare > 0) or (compare_values < 0 and item_compare < 0):
                logger.info(f"image_size:{item_image_aspectRatio}")
                self.remove_item(item)

        self.show_toast_success(title="过滤完成", message=f"移除宽高比{'小于' if compare_values > 0 else '小于'}{ft_aspectRatio}完成")

    def ft_size(self, compare_values):
        """
        过滤宽或高大于设置数值的文件
        :param compare_values: 过滤参数,需要过滤大于设置值的传入正数,小于传入负数
        """
        logger.info("按照宽高尺寸过滤文件列表")
        check_w = self.check_w.get()  # 过滤长
        check_h = self.check_h.get()  # 过滤宽
        ft_status_w = check_w not in ["0", ""]
        ft_status_h = check_h not in ["0", ""]
        if not ft_status_w and not ft_status_h:
            logger.warning("宽或者高都未填写或填写0")
            self.show_toast_error(title="过滤宽高值错误", message="过滤的宽高值不能都填写0或空")
            return

        less_more_str = '大于' if compare_values > 0 else '小于'
        logger.info(f"移除{f'宽{less_more_str}{check_w}' if ft_status_w else ''}{f'高{less_more_str}{check_h}' if ft_status_h else ''}")

        image_tree: ttk.Treeview = config["file_tree"]["image_tree"]  # 文件树
        all_item = list(image_tree.get_children())  # 所有item
        if not all_item:
            logger.info("已添加文件列表为空,不执行过滤")
            return

        for item in all_item:
            # 图片宽高
            item_image_width = int(image_tree.item(item, "values")[1].split("x")[0])
            item_image_height = int(image_tree.item(item, "values")[1].split("x")[1])

            if ft_status_w and not ft_status_h:
                # 只移除宽超限
                check_w = int(check_w)
                difference_w = item_image_width - check_w
                if (compare_values > 0 and difference_w > 0) or (compare_values < 0 and difference_w < 0):
                    self.remove_item(item)

            elif not ft_status_w and ft_status_h:
                # 只移除高超限
                check_h = int(check_h)
                difference_h = item_image_height - check_h
                if (compare_values > 0 and difference_h > 0) or (compare_values < 0 and difference_h < 0):
                    self.remove_item(item)

            elif ft_status_w and ft_status_h:
                # 移除宽高都超限
                check_w = int(check_w)
                check_h = int(check_h)
                difference_w = item_image_width - check_w
                difference_h = item_image_height - check_h
                if (compare_values > 0 and difference_w > 0 and difference_h > 0) and \
                        (compare_values < 0 and difference_w < 0 and difference_h < 0):
                    self.remove_item(item)

        self.show_toast_success(title="过滤完成", message=f"移除{f'宽{less_more_str}{check_w}' if ft_status_w else ''}{f'高{less_more_str}{check_h}' if ft_status_h else ''}完成")

    def ft_st_size(self, compare_values):
        """
        过滤文件大小大于或小于设置数值的文件
        :param compare_values: 过滤参数,需要过滤大于设置值的传入正数,小于传入负数
        """
        logger.info("按照文件大小过滤列表文件")
        st_size_mb = self.st_size_mb.get()  # 文件大小mb
        st_size_kb = self.st_size_kb.get()  # 文件大小kb
        if st_size_mb in ["0", ""]:
            st_size_mb = 0
        if st_size_kb in ["0", ""]:
            st_size_kb = 0
        if not st_size_mb and not st_size_kb:
            self.show_toast_error(title="参数错误", message="请先填写需要过滤文件大小,kb或mb最少需要填写一个")
            return

        st_size: float = float(st_size_kb) * 1024 + float(st_size_mb) * 1024 ** 2
        logger.info(f"移除文件大小{'大于' if compare_values > 0 else '小于'}{st_size_mb}mb {st_size_kb}kb,{st_size}")

        image_tree: ttk.Treeview = config["file_tree"]["image_tree"]  # 文件树
        all_item = list(image_tree.get_children())  # 所有item
        if not all_item:
            logger.info("已添加文件列表为空,不执行过滤")
            return

        for item in all_item:
            item_st_size = os.stat(image_tree.item(item, "values")[4]).st_size
            difference_st_size = item_st_size - st_size
            if (difference_st_size > 0 and compare_values > 0) or (difference_st_size < 0 and compare_values < 0):
                self.remove_item(item)

        self.show_toast_success(title="过滤完成", message=f"移除文件大小{'大于' if compare_values > 0 else '小于'}{st_size_mb}mb {st_size_kb}kb, 合计{st_size}b完成")

    def ft_file_mode(self):
        """移除选择模式文件"""
        logger.info("按照显示模式过滤列表文件")
        status_1: bool = self.status_1.get()
        status_L: bool = self.status_L.get()
        status_P: bool = self.status_P.get()
        status_I: bool = self.status_I.get()
        status_F: bool = self.status_F.get()
        status_RGB: bool = self.status_RGB.get()
        status_RGBA: bool = self.status_RGBA.get()
        status_YCbCr: bool = self.status_YCbCr.get()
        status_LAB: bool = self.status_LAB.get()
        status_HSV: bool = self.status_HSV.get()
        status_CMYK: bool = self.status_CMYK.get()
        remove_mode_list = list()
        if status_1:
            remove_mode_list.append("1")

        if status_L:
            remove_mode_list.append("L")
            remove_mode_list.append("l")

        if status_P:
            remove_mode_list.append("P")
            remove_mode_list.append("p")

        if status_I:
            remove_mode_list.append("I")
            remove_mode_list.append("i")

        if status_F:
            remove_mode_list.append("F")
            remove_mode_list.append("f")

        if status_RGB:
            remove_mode_list.append("RGB")
            remove_mode_list.append("rgb")

        if status_RGBA:
            remove_mode_list.append("RGBA")
            remove_mode_list.append("rgba")

        if status_YCbCr:
            remove_mode_list.append("YCbCr")
            remove_mode_list.append("ycbcr")
            remove_mode_list.append("YCBCR")

        if status_LAB:
            remove_mode_list.append("LAB")
            remove_mode_list.append("lab")

        if status_HSV:
            remove_mode_list.append("HSV")
            remove_mode_list.append("hsv")

        if status_CMYK:
            remove_mode_list.append("CMYK")
            remove_mode_list.append("cmyk")

        if not remove_mode_list:
            logger.info("未勾选过滤模式")
            self.show_toast_error(title="未勾选过滤模式", message="请先勾选需要过滤的模式")
            return

        logger.info(f"移除图像{', '.join(remove_mode_list)}显示模式")

        image_tree: ttk.Treeview = config["file_tree"]["image_tree"]  # 文件树
        all_item = list(image_tree.get_children())  # 所有item
        if not all_item:
            logger.info("已添加文件列表为空,不执行过滤")
            return

        for item in all_item:
            item_image_mode = image_tree.item(item, "values")[2]
            if item_image_mode in remove_mode_list:
                self.remove_item(item)

        self.show_toast_success(title="过滤完成", message=f"移除图像{', '.join(remove_mode_list)}显示模式完成")

    @staticmethod
    def updata_quality_scale(slider_value):
        """把质量值设置为整数"""
        quality_value = int(float(slider_value))
        config["compress"]["quality"].set(quality_value)

    @staticmethod
    def updata_size_show():
        """尺寸开关更新回调"""
        logger.info("尺寸开关更新回调")
        size_show_state = config["size"]["state"].get()
        equal_ratio: ttk.Radiobutton = config["size"]["equal_ratio"]  # 等比
        cut: ttk.Radiobutton = config["size"]["cut"]  # 剪裁
        stretch: ttk.Radiobutton = config["size"]["stretch"]  # 拉伸
        width_entry: ttk.Entry = config["size"]["width_entry"]  # 宽
        height_entry: ttk.Entry = config["size"]["height_entry"]  # 高

        if size_show_state:  # 如果开关开启
            logger.info("启用三个尺寸选项开关,启用宽高输入框")
            equal_ratio.config(state="normal")
            cut.config(state="normal")
            stretch.config(state="normal")
            width_entry.config(state="normal")
            height_entry.config(state="normal")

        elif not size_show_state:  # 如果开关关闭
            logger.info("禁用三个尺寸选项开关,禁用宽高输入框")
            equal_ratio.config(state="disabled")
            cut.config(state="disabled")
            stretch.config(state="disabled")
            width_entry.config(state="disabled")
            height_entry.config(state="disabled")

    @staticmethod
    def updata_format_show():
        """格式开关更新回调"""
        logger.info("格式开关更新回调")
        format_show_state = config["format"]["state"].get()
        format_combobox = config["format"]["format_combobox"]
        if format_show_state:
            logger.info("启用格式下拉框,启用gif帧间隔,启用ico格式输入框")
            format_combobox.config(state="readonly")  # 格式下拉框
            config["format"]["gif_framerate_Entry"].config(state="normal")  # gif帧间隔下拉框
            config["format"]["ico_size_text"].config(state="normal")  # ico格式输入框

        elif not format_show_state:
            logger.info("禁用格式下拉框,禁用gif帧间隔,禁用ico格式输入框")
            format_combobox.config(state="disable")
            config["format"]["gif_framerate_Entry"].config(state="disable")
            config["format"]["ico_size_text"].config(state="disable")

    @staticmethod
    def updata_compress_show():
        """压缩开关更新回调"""
        logger.info("压缩开关更新回调")
        compress_show_state = config["compress"]["state"].get()  # 显示开关
        quality_scale = config["compress"]["quality_scale"]  # 滑块
        if compress_show_state:
            logger.info("更新滑块为启用")
            quality_scale.config(state="normal")
        elif not compress_show_state:
            logger.info("更新滑块为禁用")
            quality_scale.config(state="disable")

    @staticmethod
    def updata_diy_dir():
        """输出选择自定义时回调"""
        logger.info("输出选择自定义时回调")
        output_select: ttk.Radiobutton = config["save_conf"]["output_select"].get()  # 保存单选框绑定值
        dir_structure: ttk.Checkbutton = config["save_conf"]["dir_structure"]  # 保持目录结构
        check_save_path: ttk.Button = config["save_conf"]["check_save_path"]  # 选择目录按钮
        if output_select == "diy_dir_check":  # 原目录-same_dir_check
            logger.info("设置保持目录结构和选择保存目录按钮开")
            dir_structure.place(relx=0.03, rely=0.5, relwidth=0.6, relheight=0.15)  # 保持目录结构选项
            check_save_path.place(relx=0.03, rely=0.65, relwidth=0.94, relheight=0.15)  # 目录提示和选择label

        elif output_select == "same_dir_check":  # 自定义-diy_dir_check
            logger.info("设置保持目录结构和选择保存目录按钮关")
            dir_structure.place_forget()  # 保持目录结构选项
            check_save_path.place_forget()  # 目录提示和选择label

    @staticmethod
    def updata_Combobox_format(event):
        """输出格式下拉框更新回调"""
        logger.info("输出格式下拉框更新回调")
        select_format: str = event.widget.get()
        format_combobox_tips = config["format"]["format_combobox_tips"]  # 格式下拉框提示对象
        ico_size_text: ttk.ScrolledText = config["format"]["ico_size_text"]  # ico尺寸text
        compress_show: ttk.Checkbutton = config["compress"]["compress_show"]  # 质量区域开关
        compress_show_state = config["compress"]["state"].get()  # 质量区域显示开关
        quality_scale = config["compress"]["quality_scale"]  # 质量滑块
        gif_area: ttk.Frame = config["format"]["gif_area"]  # gif帧间隔下拉列表

        ico_size_text.place_forget()  # ico尺寸text
        compress_show.config(state="disable")  # 质量区域开关
        gif_area.place_forget()

        logger.info(f"选择输出{select_format}格式")

        if select_format == "ico":

            format_combobox_tips.text = "当前选择转换为ico格式,当左侧尺寸开关打开时会先将原图转换为指定尺寸,后在转换尺寸后的图像基础上基本ico转换,多尺寸以下方输入框的多尺寸参数作为尺寸\n" \
                                        "ico格式支持最大尺寸参数256*256,大于该尺寸将会忽略,尺寸宽高仅支持处输入大于零的整数,多尺寸换行填写"
            ico_size_text.place(relx=0.03, rely=0.4, relwidth=0.8, relheight=0.59)
            logger.info(f"显示ico格式尺寸编辑输入框,禁用压缩选项")
            if compress_show_state:  # 如果质量区域开关开着则禁用滑块和勾选
                compress_show.config(state="disable")
                quality_scale.config(state="disable")

        elif select_format == "gif":
            format_combobox_tips.text = "当前选择转换为gif,会将上方文件列表所有图像合成一个gif\n" \
                                        "如打开了左侧尺寸开关则会将列表中所有图片转换为统一尺寸后合成一个gif,如不统一尺寸请尽量避免添加多个不同尺寸图像合成,否则可能会导致输出图像出现异常显示\n" \
                                        "需要注意尽量避免将多种显示模式(如1/L等)图片添加到同一个gif,可能会导致无法正常合成"

            gif_area.place(relx=0.03, rely=0.6, relwidth=1, relheight=0.18)

            logger.info(f"显示gif格式帧速率输入框,禁用压缩选项")

            if compress_show_state:
                compress_show.config(state="disable")
                quality_scale.config(state="disable")

        elif select_format == "bmp":
            format_combobox_tips.text = "当前选择bmp无损格式"
            logger.info(f"bmp格式,禁用压缩选项")

            if compress_show_state:
                compress_show.config(state="disable")
                quality_scale.config(state="disable")

        elif select_format == "jpg":
            format_combobox_tips.text = "当前选择jpg格式,可打开右侧质量开关调节滑块更改转换后的图片质量"

            logger.info(f"启用压缩选项,并设置质量值可选0-95,默认75")

            compress_show.config(state="normal")

            config["compress"]["quality"].set(75)  # 默认75
            config["compress"]["quality_scale"].config(from_=0, to=95)  # 0-95

        elif select_format == "png":
            format_combobox_tips.text = "当前选择png格式,可打开右侧质量开关调节滑块更改转换后的图片质量"
            logger.info(f"启用压缩选项,并设置质量值可选0-9,默认6")

            compress_show.config(state="normal")

            config["compress"]["quality"].set(6)  # 默认6
            config["compress"]["quality_scale"].config(from_=9, to=0)  # 0-100

        elif select_format == "webp":
            format_combobox_tips.text = "当前选择webp格式,可打开右侧质量开关调节滑块更改转换后的图片质量\n" \
                                        "注意！当前暂不支持转换为多帧的webp动图"
            logger.info(f"启用压缩选项,并设置质量值可选1-100,默认80")

            compress_show.config(state="normal")

            config["compress"]["quality"].set(80)  # 默认80
            config["compress"]["quality_scale"].config(from_=1, to=100)  # 0-100

    @staticmethod
    def get_ico_sizes():
        """获取输出ico的尺寸列表"""
        logger.info("获取ico尺寸框尺寸")
        sizes_str: str = config["format"]["ico_size_text"].get("0.0", END)
        logger.info(f"尺寸框解析文本:{', '.join(sizes_str.splitlines())}")
        _sizes = list()
        for size in sizes_str.splitlines():  # 按照行切割
            if size == "":  # 空行跳过
                logger.info("空行跳过")
                continue
            img_width_img_height = size.split("*")
            if len(img_width_img_height) != 2:  # 长度不为2错误,跳过
                logger.error(f"解析错误单行长宽解析出多个:{img_width_img_height}")
                raise ValueError

            img_width, img_height = img_width_img_height
            img_width = int(img_width)
            img_height = int(img_height)

            if img_width <= 0 or img_height <= 0:
                logger.error(f"解析错误长或宽小于0:({img_width}, {img_height})")
                raise ValueError

            _sizes.append((img_width, img_height))

        if len(_sizes) == 0:  # 输入错误的信息
            logger.error(f"解析到总尺寸个数为0:")
            raise ValueError

        return _sizes

    def start_main(self):
        """开始转换按钮回调"""
        logger.info("开始转换按钮回调,准备开始处理")
        image_tree: ttk.Treeview = config["file_tree"]["image_tree"]  # 文件树
        all_item = list(image_tree.get_children())  # 所有item

        size_state: bool = config["size"]["state"].get()  # bool尺寸修改开关
        resize_option: str = config["size"]["option"].get() if size_state else ""  # 修改尺寸选项equal_ratio等比/cut剪裁/stretch拉伸
        resize_width = config["size"]["width"].get().strip(' ')  # 修改后宽
        resize_height = config["size"]["height"].get().strip(' ')  # 修改后高
        if size_state:
            if resize_width == "" or resize_height == "":
                logger.warning("未填写宽或高")
                Messagebox.show_error(title="未填写宽或高", message="请正确填写转换尺寸宽高参数", alert=True, position=(835, 475))
                return
            elif resize_width == "0" or resize_height == "0":
                logger.warning("宽或高填写0")
                Messagebox.show_error(title="宽或高填写0", message="转换尺寸宽和高仅支持填写大于0的数字", alert=True, position=(835, 475))
                return

        format_state: bool = config["format"]["state"].get()  # bool格式开关
        image_format: str = config["format"]["format_combobox"].get()  # 输出格式
        gif_time = config["format"]["gif_framerate_Entry"].get()  # gif帧间隔
        if format_state and gif_time in ["", "0"]:
            Messagebox.show_error(title="帧间隔错误", message="gif帧间隔请输入大于0的数字后再次尝试", alert=True, position=(835, 475))
            logger.warning("if帧间隔空或输入0")
            return

        try:
            ico_sizes = self.get_ico_sizes()  # ico尺寸列表
        except ValueError:
            logger.warning("ico格式填写错误,解析失败 停止处理")
            Messagebox.show_error(title="尺寸格式填写错误", message="ico尺寸填写错误,请正确填写", alert=True, position=(835, 475))
            return
        except (OSError, Exception):
            logger.warning("ico格式解析遇到未知错误 停止处理")
            return

        compress_state: bool = config["compress"]["state"].get()  # bool压缩开关
        quality_scale: int = int(config["compress"]["quality_scale"].get())  # 输出质量值

        save_option: str = config["save_conf"]["output_select"].get()  # 保存选项same_dir_check原目录/diy_dir_check自定义目录
        dir_structure: bool = config["save_conf"]["d_structure"].get()  # bool保持目录结构开关
        save_path: str = config["save_conf"]["save_path"].get()  # 保存目录
        # 异常处理======================================================================================================
        if not all_item:
            logger.warning("未添加图像文件")
            Messagebox.show_error(title="未添加文件", message="请添加需要处理的图像文件", alert=True, position=(835, 475))
            return

        if not size_state and not format_state:
            logger.warning("未选择修改项")
            Messagebox.show_error(title="未选择修改项", message="修改格式或尺寸必须保留一个", alert=True, position=(835, 475))
            return

        if save_path == "" and save_option == "diy_dir_check":
            logger.warning("未设置保存目录")
            Messagebox.show_error(title="未设置保存目录", message="请选择保存目录", alert=True, position=(835, 475))
            return

        if format_state and image_format == "":
            logger.warning("未选择输出格式")
            Messagebox.show_error(title="未选择输出格式", message="您勾选了修改格式但未选择输出格式", alert=True, position=(835, 475))
            return

        # 开始处理======================================================================================================
        def start():
            if format_state and image_format in ["gif", "GIF"]:  # gif格式
                logger.info(f"修改为gif保存,帧间隔:{gif_time}")
                gif_duration = int(gif_time)

                # 选中第一个预览
                image_tree.selection_set(all_item[0])
                time.sleep(1)
                logger.info("选中第一个预览主图}")

                # 图像地址
                first_image_path = image_tree.item(all_item[0], "values")[4]

                output_path = self.get_save_path(image_path=first_image_path,
                                                 save_path=save_path,
                                                 image_format="gif",
                                                 same_dir=False if save_option == "diy_dir_check" else True,
                                                 dir_structure=dir_structure)
                images = [image_tree.item(itemid, "values")[4] for itemid in all_item]

                try:
                    if size_state:
                        image_size = (int(resize_width), int(resize_height))
                        gif_duration = int(gif_duration)
                        self.save_gif(images, output_path, image_size, resize_option, gif_duration)
                    else:
                        self.save_gif(images, output_path, (0, 0), duration=gif_duration)

                except UnidentifiedImageError:
                    logger.error("合成图像中有文件非图像文件")
                    self.show_toast_error(title="添加文件错误", message="合成图像中有文件非图像文件")
                    return

                except FileNotFoundError:
                    logger.error("合成图像中部分文件找不到")
                    self.show_toast_error(title="文件错误", message="合成图像中部分文件找不到")
                    return

                except DecompressionBombWarning:
                    logger.error("合成图像中部分文件超过10^9个像素点,怀疑为图像炸弹")
                    self.show_toast_error(title="错误", message=f"合成图像中部分文件超过10^9个像素点,怀疑为图像炸弹")
                    return

                except (ValueError, BaseException):
                    logger.error(f"遇到合成图像未知异常,合成失败!将如1/L/I等模式的图片同时合成转换可能会出问题")
                    self.show_toast_error(title="异常错误", message="遇到合成图像未知异常,合成失败!将如1/L/I等模式的图片同时合成转换可能会出问题")
                    return

                # 处理完成通知
                self.show_image_toast(output_path)

            else:  # 非gif
                for item in all_item:
                    image_tree.selection_set(item)  # 选中准备处理项
                    image_path = image_tree.item(item, "values")[4]
                    logger.info(f"处理文件:{image_path}")

                    output_path = self.get_save_path(image_path=image_path,
                                                     save_path=save_path,
                                                     image_format=image_format if format_state and image_format else "",
                                                     same_dir=False if save_option == "diy_dir_check" else True,
                                                     dir_structure=dir_structure)

                    try:
                        if size_state and not format_state:
                            image_size = (int(resize_width), int(resize_height))
                            logger.info("只修改尺寸")
                            # equal_ratio等比/cut剪裁/stretch拉伸
                            if resize_option == "equal_ratio":  # 等比
                                logger.info(f"等比缩放,保存:{output_path}")
                                image = self.get_Image_equal(image_path, image_size)
                                self.save_any(image, output_path)

                            elif resize_option == "cut":  # 剪裁
                                logger.info(f"剪裁缩放,保存:{output_path}")
                                image = self.get_Image_box(image_path, image_size)
                                self.save_any(image, output_path)

                            elif resize_option == "stretch":  # 拉伸
                                logger.info(f"拉伸缩放,保存:{output_path}")
                                image = self.get_Image_resize(image_path, image_size)
                                self.save_any(image, output_path)

                            else:
                                logger.error("选择裁剪方式错误,不应该走到这")
                                return

                        elif not size_state and format_state:
                            logger.info("只修改格式")
                            if image_format == "png":
                                logger.info(f"修改为png保存:{output_path}")
                                if compress_state:
                                    logger.info(f"保存压缩参数:{quality_scale}")
                                    self.save_png(image_path, output_path, quality_scale)
                                else:
                                    logger.info("默认压缩参数保存")
                                    self.save_png(image_path, output_path)

                            elif image_format == "jpg":
                                logger.info(f"修改为jpg保存:{output_path}")
                                if compress_state:
                                    logger.info(f"保存压缩参数:{quality_scale}")
                                    self.save_jpg(image_path, output_path, quality_scale)
                                else:
                                    logger.info("默认压缩参数保存")
                                    self.save_jpg(image_path, output_path)

                            elif image_format == "webp":
                                logger.info(f"修改为webp保存:{output_path}")
                                if compress_state:
                                    logger.info(f"保存压缩参数:{quality_scale}")
                                    self.save_webp(image_path, output_path, quality_scale)
                                else:
                                    logger.info("默认压缩参数保存")
                                    self.save_webp(image_path, output_path)

                            elif image_format == "ico":
                                logger.info(f"修改为ico保存:{output_path}, 修改尺寸:{ico_sizes}")
                                self.save_ico(image_path, output_path, ico_sizes)

                            elif image_format == "bmp":
                                logger.info(f"修改为bmp保存:{output_path}")
                                self.save_bmp(image_path, output_path)

                            else:
                                logger.info(f"选择格式错误,不知道为啥走到这里了")
                                return

                        elif size_state and format_state:
                            image_size = (int(resize_width), int(resize_height))
                            logger.info("同时修改尺寸和格式")
                            # 图片处理===================================================================================
                            if resize_option == "equal_ratio":  # 等比
                                logger.info("修改尺寸-等比缩放")
                                image = self.get_Image_equal(image_path, image_size)

                            elif resize_option == "stretch":  # 拉伸
                                logger.info("修改尺寸-拉伸缩放")
                                image = self.get_Image_resize(image_path, image_size)

                            elif resize_option == "cut":  # 剪裁
                                logger.info("修改尺寸-剪裁缩放")
                                image = self.get_Image_box(image_path, image_size)

                            else:
                                logger.error("选择裁剪方式错误,不应该走到这")
                                return
                                # 保存==================================================================================
                            logger.info("修改格式并保存")
                            if image_format == "png":
                                logger.info(f"修改为png保存:{output_path}")
                                if compress_state:
                                    self.save_png(image, output_path, quality_scale)
                                else:
                                    self.save_png(image, output_path)

                            elif image_format == "jpg":
                                logger.info(f"修改为jpg保存:{output_path}")
                                if compress_state:
                                    self.save_jpg(image, output_path, quality_scale)
                                else:
                                    self.save_jpg(image, output_path)

                            elif image_format == "webp":
                                logger.info(f"修改为webp保存:{output_path}")
                                if compress_state:
                                    self.save_webp(image, output_path, quality_scale)
                                else:
                                    self.save_webp(image, output_path)

                            elif image_format == "ico":
                                logger.info(f"修改为ico保存:{output_path}")
                                self.save_ico(image, output_path, ico_sizes)

                            elif image_format == "bmp":
                                logger.info(f"修改为bmp保存:{output_path}")
                                self.save_bmp(image, output_path)

                            else:
                                self.show_toast_error(title="格式错误", message=f"选择格式错误,不知道为啥走到这里了:{image_path}")
                                logger.error(f"选择格式错误,不知道为啥走到这里了")
                                return

                                # 处理完成通知
                        self.show_image_toast(output_path)
                    except UnidentifiedImageError:
                        logger.error(f"文件非图像文件,跳过:{image_path}")
                        self.show_toast_error(title="文件错误", message=f"文件非图像文件,跳过:{image_path}")
                        continue

                    except FileNotFoundError:
                        logger.error(f"找不到文件,跳过:{image_path}")
                        self.show_toast_error(title="文件错误", message=f"找不到文件,跳过:{image_path}")
                        continue

                    except DecompressionBombWarning:
                        logger.error(f"文件太大超过10^9个像素点,怀疑为图像炸弹,跳过:{image_path}")
                        self.show_toast_error(title="错误", message=f"文件太大超过10^9个像素点,怀疑为图像炸弹,跳过:{image_path}")
                        continue

                    except (ValueError, BaseException):
                        logger.error(f"合成图像遇到未知异常,选择的图片模式和目标格式支持显示模式不兼容,如1显示模式的转换是不可逆的")
                        self.show_toast_error(title="未知错误", message=f"合成图像遇到未知异常,选择的图片模式和目标格式支持显示模式不兼容,如1显示模式的转换是不可逆的,跳过:{image_path}")
                        continue

        # 独立线程去转换,无法在子线程弹窗,改用系统通知
        thread = Thread(target=start)
        thread.setDaemon(True)
        thread.start()
        # 主线程去转换,转换过程会卡顿
        # self.root.after_idle(start)


if __name__ == '__main__':
    root = ttk.Window(themename="cyborg")
    ImageTools(root)

    # 主循环
    logger.success("进入主循环")
    root.mainloop()
